"""
PDF splitting functionality module.
"""

import os
import re
from typing import List, Dict, Tuple, Optional, Callable, Any
from pathlib import Path
import logging
from dataclasses import dataclass
from enum import Enum

import fitz  # PyMuPDF
from PyQt6.QtCore import QObject, pyqtSignal, QThread

from .pdf_processor import PDFProcessor


class SplitMode(Enum):
    """Splitting modes."""
    BY_RANGE = "range"
    BY_PAGES = "pages"
    BY_BOOKMARK = "bookmark"
    BY_SELECTION = "selection"
    BY_SIZE = "size"


@dataclass
class SplitRange:
    """Represents a range of pages."""
    start: int
    end: int  # Inclusive
    label: Optional[str] = None


@dataclass
class SplitResult:
    """Result of a split operation."""
    success: bool
    file_path: str
    pages: List[int]
    error: Optional[str] = None


class SplitWorker(QObject):
    """Worker for performing PDF splitting in background thread."""
    progress = pyqtSignal(int)  # Progress percentage
    finished = pyqtSignal(list)  # List of SplitResult
    error = pyqtSignal(str)  # Error message

    def __init__(self, processor: PDFProcessor, mode: SplitMode, **kwargs):
        super().__init__()
        self.processor = processor
        self.mode = mode
        self.kwargs = kwargs
        self._is_cancelled = False

    def cancel(self):
        """Cancel the split operation."""
        self._is_cancelled = True

    def run(self):
        """Execute the split operation."""
        try:
            splitter = PDFSplitter(self.processor)

            if self.mode == SplitMode.BY_RANGE:
                ranges = self.kwargs.get('ranges', [])
                output_dir = self.kwargs.get('output_dir', '')
                results = splitter.split_by_range(ranges, output_dir, self.progress.emit)
            elif self.mode == SplitMode.BY_PAGES:
                pages_per_file = self.kwargs.get('pages_per_file', 1)
                output_dir = self.kwargs.get('output_dir', '')
                results = splitter.split_by_pages(pages_per_file, output_dir, self.progress.emit)
            elif self.mode == SplitMode.BY_BOOKMARK:
                level = self.kwargs.get('level', 1)
                output_dir = self.kwargs.get('output_dir', '')
                results = splitter.split_by_outline(output_dir, level, self.progress.emit)
            elif self.mode == SplitMode.BY_SIZE:
                max_size_mb = self.kwargs.get('max_size_mb', 10.0)
                output_dir = self.kwargs.get('output_dir', '')
                results = splitter.split_by_size(max_size_mb, output_dir, self.progress.emit)
            else:
                raise ValueError(f"Unsupported split mode: {self.mode}")

            if not self._is_cancelled:
                self.finished.emit(results)
        except Exception as e:
            self.error.emit(str(e))


class PDFSplitter(QObject):
    """PDF document splitting functionality."""

    def __init__(self, processor: PDFProcessor):
        super().__init__()
        self.processor = processor
        self._logger = logging.getLogger(__name__)

    def parse_page_ranges(self, range_text: str, total_pages: int) -> List[SplitRange]:
        """Parse page range text into SplitRange objects.

        Args:
            range_text: Text like "1-5, 10, 15-20"
            total_pages: Total number of pages in the document

        Returns:
            List of SplitRange objects

        Raises:
            ValueError: If range text is invalid
        """
        if not range_text.strip():
            raise ValueError("No page ranges specified")

        ranges = []
        # Split by comma or semicolon
        parts = re.split(r'[,;]', range_text)

        for part in parts:
            part = part.strip()
            if not part:
                continue

            # Parse single page or range
            if '-' in part:
                # Range like "1-5"
                start_str, end_str = part.split('-', 1)
                start = self._parse_page_number(start_str, total_pages)
                end = self._parse_page_number(end_str, total_pages)

                if start > end:
                    raise ValueError(f"Invalid range: {part} (start > end)")
            else:
                # Single page
                start = self._parse_page_number(part, total_pages)
                end = start

            ranges.append(SplitRange(start, end))

        if not ranges:
            raise ValueError("No valid page ranges found")

        return ranges

    def _parse_page_number(self, text: str, total_pages: int) -> int:
        """Parse a page number from text.

        Args:
            text: Page number text
            total_pages: Total number of pages

        Returns:
            1-based page index

        Raises:
            ValueError: If page number is invalid
        """
        try:
            page_num = int(text)
        except ValueError:
            raise ValueError(f"Invalid page number: {text}")

        if page_num < 1:
            raise ValueError("Page numbers must be greater than 0")
        if page_num > total_pages:
            raise ValueError(f"Page number {page_num} exceeds total pages ({total_pages})")

        return page_num

    def split_by_range(self, ranges: List[SplitRange], output_dir: str,
                      progress_callback: Optional[Callable[[int], None]] = None) -> List[SplitResult]:
        """Split PDF by specified page ranges.

        Args:
            ranges: List of page ranges
            output_dir: Output directory for split files
            progress_callback: Callback for progress updates

        Returns:
            List of SplitResult objects
        """
        if not self.processor.is_loaded():
            raise RuntimeError("No document loaded")

        results = []
        total_operations = len(ranges)

        for i, split_range in enumerate(ranges):
            if progress_callback:
                progress_callback(int(i * 100 / total_operations))

            # Generate output filename
            output_path = self._generate_output_filename(
                output_dir, split_range, f"range_{split_range.start}-{split_range.end}"
            )

            try:
                # Extract pages
                pages_to_extract = list(range(split_range.start - 1, split_range.end))
                result_path = self.extract_pages(pages_to_extract, output_path)

                results.append(SplitResult(
                    success=True,
                    file_path=result_path,
                    pages=pages_to_extract
                ))

            except Exception as e:
                self._logger.error(f"Failed to split range {split_range.start}-{split_range.end}: {e}")
                results.append(SplitResult(
                    success=False,
                    file_path=output_path,
                    pages=[],
                    error=str(e)
                ))

        if progress_callback:
            progress_callback(100)

        return results

    def split_by_pages(self, pages_per_file: int, output_dir: str,
                      progress_callback: Optional[Callable[[int], None]] = None) -> List[SplitResult]:
        """Split PDF by fixed number of pages per file.

        Args:
            pages_per_file: Number of pages per output file
            output_dir: Output directory
            progress_callback: Callback for progress updates

        Returns:
            List of SplitResult objects
        """
        if not self.processor.is_loaded():
            raise RuntimeError("No document loaded")

        if pages_per_file < 1:
            raise ValueError("Pages per file must be at least 1")

        total_pages = self.processor.get_page_count()
        results = []

        # Calculate number of files needed
        num_files = (total_pages + pages_per_file - 1) // pages_per_file

        for i in range(num_files):
            if progress_callback:
                progress_callback(int(i * 100 / num_files))

            # Calculate range for this file
            start_page = i * pages_per_file
            end_page = min(start_page + pages_per_file, total_pages)

            # Generate output filename
            output_path = self._generate_output_filename(
                output_dir,
                SplitRange(start_page + 1, end_page),
                f"part_{i+1}"
            )

            try:
                # Extract pages
                pages_to_extract = list(range(start_page, end_page))
                result_path = self.extract_pages(pages_to_extract, output_path)

                results.append(SplitResult(
                    success=True,
                    file_path=result_path,
                    pages=pages_to_extract
                ))

            except Exception as e:
                self._logger.error(f"Failed to split part {i+1}: {e}")
                results.append(SplitResult(
                    success=False,
                    file_path=output_path,
                    pages=[],
                    error=str(e)
                ))

        if progress_callback:
            progress_callback(100)

        return results

    def split_by_outline(self, output_dir: str, level: int = 1,
                        progress_callback: Optional[Callable[[int], None]] = None) -> List[SplitResult]:
        """Split PDF by bookmarks/outline.

        Args:
            output_dir: Output directory
            level: Outline level to split by
            progress_callback: Callback for progress updates

        Returns:
            List of SplitResult objects
        """
        if not self.processor.is_loaded():
            raise RuntimeError("No document loaded")

        outline = self.processor.get_outline()
        if not outline:
            raise ValueError("Document has no bookmarks/outline")

        # Filter outline by level
        bookmarks = [item for item in outline if item.get('level', 0) == level and item.get('type') == 'page']

        if not bookmarks:
            raise ValueError(f"No bookmarks found at level {level}")

        results = []
        total_pages = self.processor.get_page_count()

        for i, bookmark in enumerate(bookmarks):
            if progress_callback:
                progress_callback(int(i * 100 / len(bookmarks)))

            # Determine page range
            start_page = bookmark['page']  # 0-based
            if i < len(bookmarks) - 1:
                end_page = bookmarks[i + 1]['page']  # Up to next bookmark
            else:
                end_page = total_pages  # End of document

            if start_page >= end_page:
                continue  # Skip empty sections

            # Generate output filename from bookmark title
            title = bookmark.get('title', f'Section_{i+1}')
            title = self._sanitize_filename(title)

            output_path = self._generate_output_filename(
                output_dir,
                SplitRange(start_page + 1, end_page),
                title
            )

            try:
                # Extract pages
                pages_to_extract = list(range(start_page, end_page))
                result_path = self.extract_pages(pages_to_extract, output_path)

                results.append(SplitResult(
                    success=True,
                    file_path=result_path,
                    pages=pages_to_extract
                ))

            except Exception as e:
                self._logger.error(f"Failed to split bookmark '{bookmark.get('title')}': {e}")
                results.append(SplitResult(
                    success=False,
                    file_path=output_path,
                    pages=[],
                    error=str(e)
                ))

        if progress_callback:
            progress_callback(100)

        return results

    def split_by_size(self, max_size_mb: float, output_dir: str,
                      progress_callback: Optional[Callable[[int], None]] = None) -> List[SplitResult]:
        """Split PDF by maximum file size.

        Args:
            max_size_mb: Maximum size per output file in MB
            output_dir: Output directory
            progress_callback: Callback for progress updates

        Returns:
            List of SplitResult objects
        """
        if not self.processor.is_loaded():
            raise RuntimeError("No document loaded")

        if max_size_mb < 0.1:  # Minimum 0.1 MB
            raise ValueError("Maximum file size must be at least 0.1 MB")

        total_pages = self.processor.get_page_count()
        results = []

        # Get size of each page (more accurate calculation)
        page_sizes = []

        # Always use exact calculation for accuracy
        try:
            self._logger.info(f"Calculating page sizes for {total_pages} pages...")
            temp_doc = fitz.open()

            for page_idx in range(total_pages):
                if progress_callback:
                    progress_callback(int(page_idx * 90 / total_pages))  # 90% for size calculation

                page = self.processor._document[page_idx]
                temp_doc.insert_pdf(self.processor._document, from_page=page_idx, to_page=page_idx)

                # Save to temporary buffer to get exact size
                import io
                buffer = io.BytesIO()

                # Use garbage collection to save options to reduce overhead
                temp_doc.save(buffer, garbage=3, deflate=True, clean=True)
                size_bytes = buffer.tell()
                page_sizes.append(size_bytes)

                # Clear temp_doc for next iteration
                temp_doc = fitz.open()

                # For large documents, optionally pause periodically
                if page_idx > 0 and page_idx % 50 == 0:
                    # Process events to keep UI responsive
                    from PyQt6.QtWidgets import QApplication
                    QApplication.processEvents()

        except Exception as e:
            self._logger.error(f"Failed to calculate exact page sizes: {e}")
            # Fallback: use file-based calculation
            self._logger.warning("Falling back to file-based size calculation...")
            try:
                # Alternative method: save pages one by one to temp files
                import tempfile
                for page_idx in range(total_pages):
                    if progress_callback:
                        progress_callback(int(page_idx * 90 / total_pages))

                    with tempfile.NamedTemporaryFile(suffix='.pdf', delete=True) as tmp:
                        temp_doc = fitz.open()
                        temp_doc.insert_pdf(self.processor._document, from_page=page_idx, to_page=page_idx)
                        temp_doc.save(tmp.name)
                        temp_doc.close()

                        # Get file size
                        size_bytes = os.path.getsize(tmp.name)
                        page_sizes.append(size_bytes)
            except Exception as e2:
                # Final fallback: use rough estimate with overhead factor
                self._logger.error(f"File-based calculation also failed: {e2}")
                total_size = os.path.getsize(self.processor.get_file_path())
                # Add overhead factor (PDFs are often larger than sum of individual pages)
                overhead_factor = 1.2  # 20% overhead for metadata, cross-references, etc.
                avg_size = (total_size * overhead_factor) / total_pages
                page_sizes = [avg_size] * total_pages
                self._logger.warning(f"Using estimated page size: {avg_size / 1024:.2f} KB per page")

        max_size_bytes = max_size_mb * 1024 * 1024  # Convert MB to bytes

        # Group pages into files based on size
        current_file_pages = []
        current_file_size = 0
        file_index = 1

        for page_idx in range(total_pages):
            if progress_callback:
                progress_callback(90 + int(page_idx * 10 / total_pages))  # 90-100% for splitting

            page_size = page_sizes[page_idx]

            # If a single page is larger than the max size, create a file with just that page
            if page_size > max_size_bytes:
                self._logger.warning(f"Page {page_idx + 1} ({page_size / 1024 / 1024:.2f} MB) exceeds max size ({max_size_mb} MB)")
                # Save current file if not empty
                if current_file_pages:
                    output_path = self._generate_output_filename(
                        output_dir,
                        SplitRange(current_file_pages[0] + 1, current_file_pages[-1] + 1),
                        f"part_{file_index}"
                    )

                    try:
                        result_path = self.extract_pages(current_file_pages, output_path)
                        results.append(SplitResult(
                            success=True,
                            file_path=result_path,
                            pages=current_file_pages.copy()
                        ))
                        file_index += 1
                    except Exception as e:
                        self._logger.error(f"Failed to create file {file_index}: {e}")
                        results.append(SplitResult(
                            success=False,
                            file_path=output_path,
                            pages=current_file_pages,
                            error=str(e)
                        ))
                    current_file_pages = []
                    current_file_size = 0

                # Create file with just the oversized page
                output_path = self._generate_output_filename(
                    output_dir,
                    SplitRange(page_idx + 1, page_idx + 1),
                    f"oversized_page_{page_idx + 1}"
                )

                try:
                    result_path = self.extract_pages([page_idx], output_path)
                    actual_size = os.path.getsize(result_path)
                    self._logger.info(f"Created oversized file: {actual_size / 1024 / 1024:.2f} MB")
                    results.append(SplitResult(
                        success=True,
                        file_path=result_path,
                        pages=[page_idx]
                    ))
                    file_index += 1
                except Exception as e:
                    self._logger.error(f"Failed to create oversized file: {e}")
                    results.append(SplitResult(
                        success=False,
                        file_path=output_path,
                        pages=[page_idx],
                        error=str(e)
                    ))
                continue

            # Check if adding this page would exceed the size limit
            if current_file_pages and (current_file_size + page_size) > max_size_bytes:
                # Create a file with current pages
                output_path = self._generate_output_filename(
                    output_dir,
                    SplitRange(current_file_pages[0] + 1, current_file_pages[-1] + 1),
                    f"part_{file_index}"
                )

                try:
                    result_path = self.extract_pages(current_file_pages, output_path)
                    actual_size = os.path.getsize(result_path)
                    self._logger.info(f"File {file_index}: {len(current_file_pages)} pages, {actual_size / 1024 / 1024:.2f} MB")
                    results.append(SplitResult(
                        success=True,
                        file_path=result_path,
                        pages=current_file_pages.copy()
                    ))
                    file_index += 1
                except Exception as e:
                    self._logger.error(f"Failed to create file {file_index}: {e}")
                    results.append(SplitResult(
                        success=False,
                        file_path=output_path,
                        pages=current_file_pages,
                        error=str(e)
                    ))

                # Start a new file
                current_file_pages = [page_idx]
                current_file_size = page_size
            else:
                # Add page to current file
                current_file_pages.append(page_idx)
                current_file_size += page_size

        # Don't forget the last file
        if current_file_pages:
            output_path = self._generate_output_filename(
                output_dir,
                SplitRange(current_file_pages[0] + 1, current_file_pages[-1] + 1),
                f"part_{file_index}"
            )

            try:
                result_path = self.extract_pages(current_file_pages, output_path)
                results.append(SplitResult(
                    success=True,
                    file_path=result_path,
                    pages=current_file_pages
                ))
            except Exception as e:
                self._logger.error(f"Failed to create final file: {e}")
                results.append(SplitResult(
                    success=False,
                    file_path=output_path,
                    pages=current_file_pages,
                    error=str(e)
                ))

        if progress_callback:
            progress_callback(100)

        return results

    def extract_pages(self, pages: List[int], output_path: str) -> str:
        """Extract specific pages to a new PDF file.

        Args:
            pages: List of page indices (0-based)
            output_path: Output file path

        Returns:
            Path to the created file

        Raises:
            RuntimeError: If extraction fails
        """
        if not self.processor.is_loaded():
            raise RuntimeError("No document loaded")

        if not pages:
            raise ValueError("No pages specified for extraction")

        # Create output directory if it doesn't exist
        os.makedirs(os.path.dirname(output_path), exist_ok=True)

        try:
            # Create new document
            new_doc = fitz.open()

            # Copy pages
            for page_idx in pages:
                if page_idx < 0 or page_idx >= self.processor.get_page_count():
                    self._logger.warning(f"Skipping invalid page index: {page_idx}")
                    continue

                page = self.processor._document[page_idx]
                new_doc.insert_pdf(self.processor._document, from_page=page_idx, to_page=page_idx)

            # Save the new document
            new_doc.save(output_path)
            new_doc.close()

            self._logger.info(f"Successfully extracted {len(pages)} pages to {output_path}")
            return output_path

        except Exception as e:
            self._logger.error(f"Failed to extract pages: {e}")
            raise RuntimeError(f"Failed to extract pages: {e}")

    def _generate_output_filename(self, output_dir: str, split_range: SplitRange,
                                  default_name: str) -> str:
        """Generate output filename for a split operation.

        Args:
            output_dir: Output directory
            split_range: Page range for this split
            default_name: Default filename base

        Returns:
            Full output file path
        """
        # Get original filename without extension
        original_path = self.processor.get_file_path()
        if original_path:
            original_name = Path(original_path).stem
        else:
            original_name = "document"

        # Use custom label if provided, otherwise use default
        if split_range.label:
            name_part = split_range.label
        else:
            name_part = default_name

        # Clean filename
        name_part = self._sanitize_filename(name_part)

        # Construct filename
        filename = f"{original_name}_{name_part}.pdf"
        output_path = os.path.join(output_dir, filename)

        # Ensure unique filename
        counter = 1
        base_path = output_path[:-4]  # Remove .pdf
        while os.path.exists(output_path):
            output_path = f"{base_path}_{counter}.pdf"
            counter += 1

        return output_path

    def _sanitize_filename(self, filename: str) -> str:
        """Remove or replace invalid characters from filename.

        Args:
            filename: Original filename

        Returns:
            Sanitized filename
        """
        # Remove or replace invalid characters
        invalid_chars = r'[<>:"/\\|?*]'
        filename = re.sub(invalid_chars, '_', filename)

        # Remove leading/trailing spaces and dots
        filename = filename.strip('. ')

        # Ensure filename is not empty
        if not filename:
            filename = "untitled"

        return filename